readonly の注意点

您所在的位置:网站首页 private static readonly readonly の注意点

readonly の注意点

2024-07-17 19:51| 来源: 网络整理| 查看: 265

概要

「定数」で、読み取り専用のフィールドが作れるという話をしました。 この時点ではまだクラスや構造体、値型と参照型の違いなどについて触れていなかったのでreadonly修飾子の簡単な紹介だけに留めましたが、 本項で改めてreadonlyについて説明します。

整数などの基本的な型に対して使う分には特に問題は起きないんですが、構造体やクラスなど、複合型に対して使うときには注意が必要です。

参照型のフィールドに対して readonly

readonlyに関して最も注意が必要な点は、readonlyは再帰的には働かないという点です。 readonlyを付けたその場所だけが読み取り専用になり、参照先などについては書き換えが可能です。

例えば以下のコードを見てください。Programクラスのフィールドcにはreadonlyが付いていますが、 cが普通に書き換え可能なクラスのフィールドなので、クラスの中身は自由に書き換えられます。

// 書き換え可能なクラス class MutableClass { // フィールドを直接公開 public int X; // 書き換え可能なプロパティ public int Y { get; set; } // フィールドの値を書き換えるメソッド public void M(int value) => X = value; } class Program { static readonly MutableClass c = new MutableClass(); static void Main() { // これは許されない。c は readonly なので、c 自体の書き換えはできない c = new MutableClass(); // けども、c の中身までは保証してない // 書き換え放題 c.X = 1; c.Y = 2; c.M(3); } }

クラスを書き換えできないように作る場合、クラス自体を書き換え不能に作りましょう。 (クラスの方で、フィールドをreadonlyにしたり、プロパティをget-onlyにします。)

値型のフィールドに対して readonly

クラス(参照型)とは対照的に、構造体(値型)の場合はデータを直接持ちます。 そのため、構造体のフィールドに対してreadonlyを付けると、構造体の中身も読み取り専用になります。 ただし、メソッドの呼び出しなどを行う際、コピーが発生するという別の注意が必要です。

例えば以下のように、readonlyが付いたフィールドc自体に加えて、cのフィールドも書き換えできません。

using System; // 書き換え可能な構造体 struct MutableStruct { // フィールドを直接公開 public int X; // フィールドの値を書き換えるメソッド public void M(int value) => X = value; } class Program { static readonly MutableStruct c = new MutableStruct(); static void Main() => Allowed(); private static void NotAllowed() { // これはもちろん許されない。c は readonly なので、c 自体の書き換えはできない c = new MutableStruct(); // 構造体の場合、フィールドに関しては readonly な性質を引き継ぐ c.X = 1; } private static void Allowed() { // でも、メソッドは呼べてしまう c.M(3); // X を 3 で上書きしているはず? Console.WriteLine(c.X); // でも、X は 0 のまま //↑のコードは、実はコピーが発生している // 以下のコードと同じ意味になる var local = c; local.M(3); Console.WriteLine(c.X); // 書き換わってるのは local (コピー)の方なので、c は書き換わらない(0) Console.WriteLine(local.X); // もちろんこっちは書き換わってる(3) } }

この例の後半を見ての通り、メソッドは呼べてしまいます。 フィールドXは書き換えれないはずなのに、そのXを書き換えているメソッドMを呼んでもエラーになりません。 C# では、こういう場合に、readonlyであることを保証しつつメソッドを呼び出せるように、フィールドを一度コピーしてから、そのコピーに対してメソッドを呼ぶということをしています。

このコピーは、万が一に備えて防衛的にコピー(defensive copy)するものです。 実際にコピーが必要かどうか(実際にメソッド内で書き換えをしているかどうか)に関わらず、常にコピーが発生します。 ソースコード上は目に見えないコピーなので、隠れたコピー(hidden copy)と呼ばれたりもします。

すなわち、コピーが発生してまずいような場合(例えば構造体のサイズが大きくてコピーにコストが掛かるとか)には、readonlyなフィールドを使うことで問題が発生することがあります。 この問題は、in引数などでも発生しまえます。 後述するreadonly structやreadonly 関数メンバーを使えばこの問題は少し緩和するので、そちらも参照してください。

構造体の this 書き換え

C# のreadonlyフィールドには少し片手落ちなところがあって、実は、構造体の場合にちょっとした問題を起こせたりします。

構造体のメソッドの中ではthisが「自分自身の参照」の意味なんですが、このthis参照は書き換えできてしまいます。 そのため、以下のように、readonlyで一見書き換えができなさそうなフィールドを書き換えてしまうことができます。

using System; struct Point { // フィールドに readonly を付けているものの… public readonly int X; public readonly int Y; public Point(int x, int y) => (X, Y) = (x, y); // this の書き換えができてしまうので、実は X, Y の書き換えが可能 public void Set(int x, int y) { // X = x; Y = y; とは書けない // でも、this 自体は書き換えられる this = new Point(x, y); } } class Program { static void Main() { var p = new Point(1, 2); // p.X = 0; とは書けない。これはちゃんとコンパイル エラーになる // でも、このメソッドは呼べるし、X, Y が書き換わる p.Set(3, 4); Console.WriteLine(p.X); // 3 Console.WriteLine(p.Y); // 4 } }

わざわざこんな紛らわしいことをしようとは思わないのでめったに問題になることはないんですが、一応は注意が必要です。 また、この問題は、次節で説明する通り、C# 7.2で少し緩和されます。

readonly struct Ver. 7.2

C# 7.2で、構造体自体にreadonly修飾を付けられるようになりました。 readonlyを付けた構造体は以下のような状態になります。

全てのフィールドに対して readonly を付けなければならなくなる get-onlyプロパティは使えます(自動生成されるフィールドがreadonlyなので問題ない) this参照もreadonly扱いされる

thisがreadonly扱いになるので、前節のようなthis書き換えの問題は起きません。

using System; // 構造体自体に readonly を付ける readonly struct Point { // フィールドには readonly が必須 public readonly int X; public readonly int Y; public Point(int x, int y) => (X, Y) = (x, y); // readonly を付けない場合と違って、以下のような this 書き換えも不可 //public void Set(int x, int y) => this = new Point(x, y); } class Program { static void Main() { var p = new Point(1, 2); // p.X = 0; とは書けない。これはちゃんとコンパイル エラーになる // p.Set(3, 4); みたいなのもダメ Console.WriteLine(p.X); // 1 しかありえない Console.WriteLine(p.Y); // 2 しかありえない } } readonly struct によるコピー回避

前述の通り、(無印の)構造体のreadonlyフィールドに対してメソッドを呼ぶと防衛的コピーが発生するという問題があります。 これに対して、readonly structであれば、このコピーを回避できます。

例えば以下のように、ほぼ同じ構造・どちらも書き換え不能な構造体を作ったとして、readonly structになっているかどうかでコピー発生の有無が変わります。

using System; // 作りとしては readonly を意図しているので、何も書き換えしない // でも、struct 自体には readonly が付いていない struct NoReadOnly { public readonly int X; public void M() { } } // NoReadOnly と作りは同じ // ちゃんと readonly struct readonly struct ReadOnly { public readonly int X; public void M() { } } class Program { static readonly NoReadOnly nro; static readonly ReadOnly ro; static void Main() { // readonly を付けなかった場合 // フィールド参照(読み取り)は問題ない Console.WriteLine(nro.X); // メソッド呼び出しが問題。ここでコピー発生 // (呼び出し側では、「M の中で特に何も書き換えていない」というのを知るすべがないので、防衛的にコピーが発生) nro.M(); // readonly を付けた場合 // これなら、M をそのまま呼んでも何も書き換わらない保証があるので、コピーは起きない ro.M(); } // これも問題あり(コピー発生) // in を付けたので readonly 扱い → M を呼ぶ際にコピー発生 static void F(in NoReadOnly x) => x.M(); // こちらも、readonly struct であれば問題なし(コピー回避) static void F(in ReadOnly x) => x.M(); }

C# 7.2 以降では、書き換えを意図していない構造体に対してはreadonly修飾を付けるのが無難でしょう。

また、「フィールド直接参照なら大丈夫だけど、メソッドを(プロパティも)呼ぶとコピー発生」という性質上、 書き換えを最初から意図している構造体の場合は、プロパティよりも、フィールドを直接publicにしてしまう方が都合がいいことがあります。

readonly参照と不変性

in引数やref readonlyで、読み取り専用の参照を作れます。 この読み取り専用参照は、「そのメソッド内で書き換えない」、「その引数・変数を通した書き換えをしない」という意思表明としては非常に有用です。 その一方で、「外で書き換わる」、「参照元の値が書き換わる」という意味で、不変性(immutability)の保証はありません。

例えば以下の例を見てください。

using System; class Program { static void Main() { _value = 0; ByVal(_value); // 0, 0 _value = 0; ByRef(_value); // 0, 1 } // 書き換えできるフィールド static int _value; // 値渡し = コピー なので、 _value 書き換えの影響は受けない static void ByVal(int value) { Console.WriteLine(value); _value++; Console.WriteLine(value); } // 参照渡しなので、 _value 書き換えの影響を受ける // in (ref readonly) であっても、immutable ではない // value を通して書き換えない保証があるだけで、別経路で書き換わることに対しては無力 static void ByRef(in int value) { Console.WriteLine(value); _value++; Console.WriteLine(value); } }

メソッドの中身としては全く同じメソッドが2つありますが、片方(ByVal)は値渡しで、もう片方(ByRef)は in 引数で整数値を受け取っています。 ByValでは、valueは値のコピーを受け取っているので、元の値の出どころとは無縁になっています。 一方、ByRefの方ではvalue自身はinが付いていて書き換えられませんが、その参照元になっている_value の方が書き換わると、valueの値も一緒に変化します。 書き換え不能(readonly)だからと言って、値の不変性(immutable)の保証はなく、こうして値が変化する場合があります。

readonly 関数メンバー Ver. 8.0

C# 8.0 で、関数メンバー単位で「フィールドを書き換えてない」ということを保証できるようになりました。 構造体全体を readonly struct にしなくても、隠れたコピー問題を避けられる機会が増えます。

以下のように、関数メンバーに readonly 修飾を付けます。

// 構造体自体は readonly にしない。 // フィールドは書き換えたい struct NonReadOnly { public float X; public float Y; // でも、このプロパティ内ではフィールドを書き換えない public float LengthSquared => X * X + Y * Y; } // NonReadOnly との差は LengthSquared の readonly の有無だけ struct ReadOnly { public float X; public float Y; // readonly 修飾でフィールドを書き換えないことを明示 public readonly float LengthSquared => X * X + Y * Y; } class Program { // こっちは、LengthSquared 内での X, Y の書き換えを恐れて隠れたコピーが発生する。 static float M(in NonReadOnly x) => x.LengthSquared; // こっちは、LengthSquared に readonly が付いているのでコピー発生しない。 static float M(in ReadOnly x) => x.LengthSquared; static void Main(string[] args) { M(new NonReadOnly { X = 1, Y = 2 }); M(new ReadOnly { X = 1, Y = 2 }); } }

隠れたコピー問題はソースコードの見た目に現れず、気づきにくい問題なので、 関数内でフィールドを書き換えていないなら積極的に readonly 修飾を付けておくべきでしょう。

ちなみに、逆に、readonly 関数メンバー内から、readonly ではないものを触ろうとしても隠れたコピーが発生します。 例えば以下のコードでは、Aのフィールドを書き換えるIncrementメソッドを、 readonly なメソッドとそうでないメソッドから呼び出してみています。

using System; struct A { public int Value; public void Increment() => Value++; } struct B { public A A; // A の非 readonly メンバーを呼ぶ。 public void Mutable() => A.Increment(); // Mutable との差は readonly 修飾が付いてるだけ。 // this が書き換わらないように、A のコピーが作られる。A 自体には変化が起きない。 public readonly void Immutable() => A.Increment(); } class Program { static void Main() { var b = new B(); Console.WriteLine(b.A.Value); // 初期状態: 0 b.Mutable(); Console.WriteLine(b.A.Value); // 意図通りの書き換え: 1 b.Immutable(); Console.WriteLine(b.A.Value); // 書き換わらない: 1 (Immutable の中で A のコピーが発生) } } 注意: 似て非なるもの(ref readonly)

この readonly 関数メンバーは、構文上、ref readonlyと似ているのでちょっと注意が必要かもしれません。

struct S { public int[] _value; // これは、読み取り専用参照を返すという意味。 // _value 配列の中身が書き換わってもらっては困る。 public ref readonly int X => ref _value[0]; // これは、S 内のフィールド(この場合 _value) を書き換えないという意味。 // _value 配列の中身が書き換わろうと知ったことではない。 public readonly ref int Y => ref _value[0]; // これは、上記2つの両方の意味。 // _value 自体も書き換わらないし、_value の中身を書き換えてもらっても困るとき用。 public readonly ref readonly int Z => ref _value[0]; }

ちなみに、プロパティの場合は get/set それぞれ別に readonly 指定ができます。 当然ですが、ほとんどの場合は「get だけが readonly」になると思われます。

struct X { int _value; public int Value { readonly get => _value; set => _value = value; } }


【本文地址】


今日新闻


推荐新闻


    CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3